"""Text analyzer: DeepSeek-powered classification with keyword fallback.

- Responsibilities (M8):
  - Build prompt from `config/categories.yaml` with instruction "仅返回 JSON，不做解释".
  - analyze_video_script(script, cfg): call API with timeout + exponential backoff.
  - Parse JSON result to fields: theme, sentiment, style, title_emotion, title_topic.
  - Fallback to keyword-based matching when API fails.

- Rules: PEP8, type annotations, logging, error isolation, no key leakage.
"""
from __future__ import annotations

import json
import logging
import os
import time
from typing import Any, Dict, List, Optional
from pathlib import Path

import yaml  # type: ignore
import requests  # type: ignore

LOGGER = logging.getLogger(__name__)


def _load_categories(categories_path: str = "config/categories.yaml") -> Dict[str, Any]:
    try:
        with open(categories_path, "r", encoding="utf-8") as f:
            data = yaml.safe_load(f) or {}
        return data or {}
    except Exception as exc:
        LOGGER.error("invalid_format: categories yaml load failed: %s", exc)
        return {}


def _get_retry_conf(cfg: Dict[str, Any]) -> Dict[str, Any]:
    r = cfg.get("processing", {}).get("retry", {})
    return {
        "max_retries": int(r.get("max_retries", 3)),
        "initial": float(r.get("backoff_initial_seconds", 1.0)),
        "multiplier": float(r.get("backoff_multiplier", 2.0)),
        "max_delay": float(r.get("backoff_max_seconds", 30.0)),
    }


def _build_prompt(script: str, categories: Dict[str, Any]) -> str:
    # Provide concise instruction, include category keys and options names as context
    def _collect_options(cat_key: str) -> List[str]:
        cats = categories.get("categories", [])
        for cat in cats:
            if cat.get("key") == cat_key:
                opts = cat.get("options")
                # options may be list[dict] or list[str]; normalize to names or keys
                if isinstance(opts, list):
                    out: List[str] = []
                    for o in opts:
                        if isinstance(o, dict):
                            name = o.get("name") or o.get("key")
                            if name:
                                out.append(str(name))
                        elif isinstance(o, str):
                            out.append(o)
                    return out
        return []

    fields = [
        ("theme", "视频主题"),
        ("sentiment", "感情色彩"),
        ("style", "语言风格"),
        ("title_emotion", "标题情感表达"),
        ("title_topic", "标题平台话题"),
    ]
    parts: List[str] = [
        "请阅读以下中文视频脚本并进行分类。",
        "严格要求：仅返回 JSON，不做解释。",
        "需要输出字段：theme, sentiment, style, title_emotion, title_topic。",
    ]
    for key, name in fields:
        opts = ", ".join(_collect_options(key))
        if opts:
            parts.append(f"{name}可选：{opts}")
    parts.append("脚本如下：")
    parts.append(script)
    return "\n".join(parts)


def _call_deepseek(prompt: str, api_conf: Dict[str, Any]) -> str:
    # Generic OpenAI-compatible Chat Completions endpoint
    base_url = api_conf.get("base_url", "https://api.deepseek.com")
    timeout = int(api_conf.get("timeout", 30))
    api_key = os.environ.get("DEEPSEEK_API_KEY", "")
    if not api_key:
        raise RuntimeError("missing_api_key: DEEPSEEK_API_KEY not set")

    url = base_url.rstrip("/") + "/v1/chat/completions"
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    }
    payload = {
        "model": api_conf.get("model", "deepseek-chat"),
        "messages": [
            {"role": "system", "content": "你是一个中文文本分类器，只返回JSON"},
            {"role": "user", "content": prompt},
        ],
        "temperature": 0,
    }
    resp = requests.post(url, headers=headers, json=payload, timeout=timeout)
    resp.raise_for_status()
    data = resp.json()
    # OpenAI style: choices[0].message.content
    content = (
        (data.get("choices") or [{}])[0].get("message") or {}
    ).get("content")
    if not content:
        raise RuntimeError("invalid_format: empty content from API")
    return str(content)


def _retry_call(op, *, max_retries: int, initial: float, multiplier: float, max_delay: float):
    attempt = 0
    delay = initial
    last_exc: Optional[Exception] = None
    while attempt <= max_retries:
        try:
            return op()
        except Exception as exc:
            last_exc = exc
            if attempt == max_retries:
                break
            time.sleep(min(delay, max_delay))
            delay = min(delay * multiplier, max_delay)
            attempt += 1
    raise last_exc or RuntimeError("processing_failed: unknown")


def _parse_json_result(text: str) -> Dict[str, str]:
    try:
        obj = json.loads(text)
        out = {
            "theme": str(obj.get("theme") or "NA"),
            "sentiment": str(obj.get("sentiment") or "NA"),
            "style": str(obj.get("style") or "NA"),
            "title_emotion": str(obj.get("title_emotion") or "NA"),
            "title_topic": str(obj.get("title_topic") or "NA"),
        }
        return out
    except Exception as exc:
        raise RuntimeError(f"invalid_format: api returned non-JSON: {exc}")


def _match_by_keywords(script: str, categories: Dict[str, Any]) -> Dict[str, str]:
    s = script.lower()
    def pick(cat_key: str) -> str:
        for cat in categories.get("categories", []):
            if cat.get("key") != cat_key:
                continue
            for opt in cat.get("options", []):
                if not isinstance(opt, dict):
                    continue
                kws = opt.get("keywords", []) or []
                for kw in kws:
                    try:
                        if str(kw).lower() in s:
                            return str(opt.get("key") or opt.get("name") or "NA")
                    except Exception:
                        continue
        return "NA"
    return {
        "theme": pick("theme"),
        "sentiment": pick("sentiment"),
        "style": pick("style"),
        "title_emotion": pick("title_emotion"),
        # title_topic usually from title; fallback via keywords in script
        "title_topic": pick("title_topic"),
    }


def analyze_video_script(script: str, cfg: Dict[str, Any]) -> Dict[str, str]:
    """Analyze script to classification dict via DeepSeek; fallback to keywords.

    Returns dict with keys: theme, sentiment, style, title_emotion, title_topic.
    """
    categories_path = cfg.get("paths", {}).get("categories_yaml", "config/categories.yaml")
    categories = _load_categories(categories_path)

    api_conf = cfg.get("apis", {}).get("deepseek", {})
    retry_conf = _get_retry_conf(cfg)

    try:
        prompt = _build_prompt(script, categories)
        text = _retry_call(
            lambda: _call_deepseek(prompt, api_conf),
            max_retries=retry_conf["max_retries"],
            initial=retry_conf["initial"],
            multiplier=retry_conf["multiplier"],
            max_delay=retry_conf["max_delay"],
        )
        return _parse_json_result(text)
    except Exception as exc:
        LOGGER.warning("deepseek_fallback: %s", exc)
        return _match_by_keywords(script, categories)
